Programming and management of experiments in oTree
Module 3: Group experiments
Group experiments
- Each player chooses independently
- Payoffs are defined by parameters and by her own choice and the choices of the other players
Matching, roles, values
- How many repetitions of an interaction are implemented?
- One shot
- Repeated
- When repeated, how are people matched?
- Partner matching
- All subjects are matched with the same partner for the entire experiment
- Random partner matching
- Subjects are randomly matched in each repetition
- Partner matching
- Which roles do they have in the interaction?
- Symmetric roles
- All subjects are identical
- Asymmetric roles
- e.g., one subject is of type A and the other is of type B
- Symmetric roles
- Which values are associated to subjects?
- Conditional on the role
- e.g., type A has an endowment of 10 and Type B has an endowment of 5
- Unconditional
- e.g., all subjects have an endowment of 10
- Conditional on the role
A working example
- Consider 8 subjects that are matched in groups of two
- One subject in each group is of type BLUE and the other of type GREEN
Repeated interaction
- When the interaction is repeated we can have 4 possible combinations of type ad matching
| Matching | |||
|---|---|---|---|
| Constant | Varying | ||
| Type | Constant | CT/CM | CT/VM |
| Varying | VT/CM | VT/VM |
- Changes in roles and matching can be explicitly modeled by design or be random
- In the code provided below we randomize
- Safe and common approach
- In the code provided below we randomize
Constant Type and Constant Matching (CT/CM)
- Round 1
- Round 2
- …
- Individuals are matched together for the entire experiment and keep the same role across repetitions
Varying Type and Constant Matching (VT/CM)
- Round 1
- Round 2
- …
- Individuals are matched together for the entire experiment but do not keep the same role across repetitions
Constant Type and Varying Matching (CT/VM)
- Round 1
- Round 2
- …
- Individuals are not matched together for the entire experiment but keep the same role across repetitions
Varying Type and Varying Matching (VT/VM)
- Round 1
- Round 2
- …
- Individuals are not matched together for the entire experiment and do not keep the same role across repetitions
Values
- Participants are generally endowed with some values (attributes)
- Unconditionally the same for all participants
- e.g., endowment in the dictator game
- Conditional upon some characteristic of the participant
- e.g., efficiency factors the are related to previous performance in a task
- Unconditionally the same for all participants
Values: example
- Green Players get an endowment of 0 Euro
- Blue players get an endowment of 10 Euro
oTree code for roles, matching, and value assignment
_init_.py
- Roles and matching are governed by the file
_init_.py- Section models
- This file manages the “structure” of your experiment
- The “database”
- 4 alternative protocols are presented here
- Constant type and constant matching (CT/CM)
- Varying type and constant matching (VT/CM)
- Constant type and varying matching (CT/VM)
- Varying type and varying matching (VT/VM)
Constant type and constant matching (CT/CM)
CT/CM
- This is the code in
_init_.py
class Constants(BaseConstants):
name_in_url = 'groups_roles'
players_per_group = 2
num_rounds = 10
matching = "Constant Type and Constant Matching (CT/CM)"
class Subsession(BaseSubsession):
pass
def creating_session(subsession):
if subsession.round_number == 1: # this way we get a fixed role across repetitions
subsession.group_randomly()
print(subsession.get_group_matrix())
for g in subsession.get_groups():
for p in g.get_players():
if p.id_in_group % 2 == 0:
p.type = 'BLUE'
p.value = cu(10) # assign the corresponding value
else:
p.type = 'GREEN'
p.value = cu(0)# assign the corresponding value
else:
subsession.group_like_round(1)
for g in subsession.get_groups():
for p in g.get_players():
p.type = p.in_round(subsession.round_number-1).type
p.value = p.in_round(subsession.round_number-1).value
class Group(BaseGroup):
pass
# needed to store values
class Player(BasePlayer):
type = models.StringField()
id_oth = models.IntegerField()
type_oth = models.StringField()
value = models.CurrencyField()
value_oth = models.CurrencyField()This is the assignment to groups and roles we get (from the POW of Player 1)
…
CT/CM: commented code
class Constants(BaseConstants):
# define here the constants of the session
name_in_url = 'groups_roles'
# label that appears in browser
players_per_group = 2
# how many players in each group (important for matching!)
num_rounds = 10
# how many repetitions (important for matching!)
matching = "Constant Type and Constant Matching (CT/CM)"
# name of the matching protocol
class Subsession(BaseSubsession):
pass
#group and types are defined in the Subsession class by the following function (method of the subsession class)
def creating_session(subsession):
#to set initial values in the subsession
#***************************************************************************
# START code to generate matching and roles in Round 1
#***************************************************************************
if subsession.round_number == 1:
# the following code is executed only id round is == 1
# round_number -> is a built-in function that gives the current round number
subsession.group_randomly()
# group_randomly() -> built-in function that shuffles players randomly
for g in subsession.get_groups():
# get_groups() -> returns a list of all the groups in the subsession.
# loop through the groups in the subsession
for p in g.get_players():
# get_players() -> returns a list of all the players in the subsession.
# loop through the players in the subsession (p is a player)
if p.id_in_group % 2 == 0:
# id_in_group -> player's attribute (unique identifier)
# if the id is even (via modulo operator)
p.type = 'BLUE'
# the participant is assigned to type "BLUE"
# type is "initialized" in class player as a string
p.value = cu(10)
# the blues are assigned an endowment of 10 points
# value is "initialized" in class player as currency
else:
# if the participant id is odd
p.participant.vars['type'] = 'GREEN'
# the participant is assigned to type "GREEN"
p.value = cu(0)
# the greens are assigned an endowment of 0 points
# value is "initialized" in class player as currency
#***************************************************************************
# END code to generate matching and roles in Round 1
#***************************************************************************
#***************************************************************************
# START code to generate matching and types in round >1
#***************************************************************************
else:
# if round is not round 1 (see the indenting)
subsession.group_like_round(1)
# perform matching like in round 1 (partner matching)
for g in subsession.get_groups():
for p in g.get_players():
p.type = p.in_round(subsession.round_number-1).type
p.value = p.in_round(subsession.round_number-1).value
#***************************************************************************
# END code to generate matching and roles
#***************************************************************************
class Player(BasePlayer):
type = models.StringField() # this is a string variable that will be filled with player's type (see above)
value = models.CurrencyField() # this is a currency variable that will be filled with player's endowment (see above)
class Group(BaseGroup):
passVarying type and constant matching (VT/CM)
VT/CM
class Constants(BaseConstants):
name_in_url = 'groups_roles'
players_per_group = 2
num_rounds = 10
matching = " Varying Type and Constant Matching (VT/CM)"
import random
class Subsession(BaseSubsession):
pass
def creating_session(subsession):
if subsession.round_number == 1: # this way we get a fixed role across repetitions
subsession.group_randomly()# built-in function
print(subsession.get_group_matrix())
rdm=random.randint(0, 1)
print(rdm)
for g in subsession.get_groups():
for p in g.get_players():
if rdm==1: #this way we randomize role accordin to id in group
if p.id_in_group % 2 == 0:
p.type = 'BLUE'
p.value = cu(10)# assign the corresponding value
else:
p.type = 'GREEN'
p.value = cu(0) # assign the corresponding value
else:
if p.id_in_group % 2 == 0:
p.type = 'GREEN'
p.value = cu(0)
else:
p.type = 'BLUE'
p.value = cu(10)
else:
subsession.group_like_round(1)
rdm=random.randint(0, 1)
print(rdm)
for g in subsession.get_groups():
for p in g.get_players():
if rdm==1: #this way we randomize role accordin to id in group
if p.id_in_group % 2 == 0:
p.type = 'BLUE'
p.value = cu(10)
else:
p.type = 'GREEN'
p.value = cu(0)
else:
if p.id_in_group % 2 == 0:
p.type = 'GREEN'
p.value = cu(0)
else:
p.type = 'BLUE'
p.value = cu(10)
print(subsession.get_group_matrix())
class Player(BasePlayer):
type = models.StringField() # this is a string variable that will be filled with player's type (see above)
value = models.CurrencyField() # this is a currency variable that will be filled with player's endowment (see above)
class Group(BaseGroup):
passThis is the assignment to groups and roles we get (from the POW of Player 1)
…
…
VT/CM: commented code
class Constants(BaseConstants):
# define here the constants of the session
name_in_url = 'groups_roles'
# label that appears in browser
players_per_group = 2
# how many players in each group (important for matching!)
num_rounds = 10
# how many repetitions (important for matching!)
matching = " Varying Type and Constant Matching (VT/CM)"
# matching protocol
import random
# import module random
class Subsession(BaseSubsession):
#group and types are defined in the Subsession class by the following function (method of the subsession class)
def creating_session(subsession):
#to set initial values in the subsession
#***************************************************************************
# START code to generate matching and types in round 1
#***************************************************************************
if subsession.round_number == 1:
# the following code is executed only id round is == 1
# round_number -> is a built-in function that gives the current round number
subsession.group_randomly()# built-in function
# group_randomly() -> built-in function that shuffles players randomly
rdm=random.randint(0, 1)
# assign a random value, either 0 or 1, to variable rdm
for g in subsession.get_groups():
#get_groups() -> returns a list of all the groups in the subsession.
# loop through the groups in the subsession
for p in g.get_players():
# get_players() -> returns a list of all the players in the subsession.
# loop through the players in the subsession (p is a player)
#***************************************************************************
# matching and types when random is 1 - > even = BLUE, odd= GREEN
#***************************************************************************
if rdm==1:
# the following code is executed if rdm is 1
if p.id_in_group % 2 == 0:
# id_in_group -> player's attribute (unique identifier)
# if the id is even (via modulo operator)
p.type = 'BLUE'
# the participant is assigned to type "BLUE"
p.value = c(10)# assign the corresponding value
# the participant is assigned the corresponding endowment
else:
# if the participant id is odd
p.type = 'GREEN'
# the participant is assigned to type "GREEN"
p.value = c(0)# assign the corresponding value
# the participant is assigned the corresponding endowment
#***************************************************************************
# matching and types when random is 1 - > even = GREEN, odd= BLUE
#***************************************************************************
else:
# the following code is executed if rdm is 0
if p.id_in_group % 2 == 0:
# see comment above
p.type = 'GREEN'
# see comment above
p.value = c(0)# assign the corresponding value
# the participant is assigned the corresponding endowment
else:
p.type = 'BLUE'
# see comment above
p.value = c(10)# assign the corresponding value
# the participant is assigned the corresponding endowment
#***************************************************************************
# END code to generate matching and types in round 1
#***************************************************************************
#***************************************************************************
# START code to generate matching and types in round >1
#***************************************************************************
else:
# if round is not round 1 (see the indenting)
subsession.group_like_round(1)
# perform matching like in round 1 (partner matching)
rdm=random.randint(0, 1)
# here we run the same code as in round 1 to randomly generate types
for g in subsession.get_groups():
for p in g.get_players():
if rdm==1:
if p.id_in_group % 2 == 0:
p.type = 'BLUE'
else:
p.type = 'GREEN'
else:
if p.id_in_group % 2 == 0:
p.type = 'GREEN'
else:
p.type = 'BLUE'
#***************************************************************************
# END code to generate matching and types
#***************************************************************************
class Group(BaseGroup):
pass
# needed to store values
class Player(BasePlayer):
type = models.StringField()
value = models.CurrencyField()Constant Type and Varying Matching (CT/VM)
CT/VM
class Constants(BaseConstants):
name_in_url = 'groups_roles'
players_per_group = 2
num_rounds = 10
matching = "Constant Type and Varying Matching (CT/VM)"
class Subsession(BaseSubsession):
pass
def creating_session(subsession):
subsession.group_randomly(fixed_id_in_group=True)# built-in function
print(subsession.get_group_matrix())
for g in subsession.get_groups():
for p in g.get_players():
if p.id_in_group % 2 == 0:
p.type = 'BLUE'
p.value = cu(10)
else:
p.type = 'GREEN'
p.value = cu(0)
#To assign values in each round, the blue get 10 and the green get 0
for p in subsession.get_players():
if p.type == 'BLUE':
p.value = cu(10)
else:
p.value = cu(0)
class Player(BasePlayer):
type = models.StringField()
class Group(BaseGroup):
passThis is the assignment to groups and roles we get (from the POW of Player 1)
…
…
CT/VM: commented code
class Constants(BaseConstants):
# define here the constants of the session
name_in_url = 'groups_roles'
# label that appears in browser
players_per_group = 2
# how many players in each group (important for matching!)
num_rounds = 10
# how many repetitions (important for matching!)
matching = "Varying Type and Varying Matching (VT/VM)"
# name of matching protocol
class Subsession(BaseSubsession):
pass
#group and types are defined in the Subsession class by the following function (method of the subsession class)
def creating_session(subsession):
#to set initial values in the subsession
subsession.group_randomly(fixed_id_in_group=True)
#group_randomly(fixed_id_in_group=True) -> built-in function that shuffles players randomly but keeps the id constant
for g in subsession.get_groups():
# get_groups() -> returns a list of all the groups in the subsession.
# loop through the groups in the subsession
for p in g.get_players():
# get_players() -> returns a list of all the players in the subsession.
# loop through the players in the subsession (p is a player)
if p.id_in_group % 2 == 0:
# id_in_group -> player's attribute (unique identifier)
# if the id is even (via modulo operator)
p.type = 'BLUE'
# the participant is assigned to type "BLUE"
p.value = c(10)
# the corresponding endowment
else:
# if the participant id is odd
p.type = 'GREEN'
# the participant is assigned to type "GREEN"
p.value = c(0)
# the corresponding endowment
class Player(BasePlayer):
type = models.StringField()
value = models.CurrencyField()
class Group(BaseGroup):
passVarying Type and Varying Matching (VT/VM)
VT/VM
class Constants(BaseConstants):
name_in_url = 'groups_roles'
players_per_group = 2
num_rounds = 10
matching = "Varying Type and Varying Matching (VT/VM)"
class Subsession(BaseSubsession):
pass
def creating_session(subsession):
subsession.group_randomly() # built-in function
rdm = random.randint(0, 1)
print(rdm)
for g in subsession.get_groups():
for p in g.get_players():
if rdm == 1: # this way we randomize role according to id in group
if p.id_in_group % 2 == 0:
p.type = 'BLUE'
p.value = cu(10)
else:
p.type = 'GREEN'
p.value = cu(0)
else:
if p.id_in_group % 2 == 0:
p.type = 'GREEN'
p.value = cu(0)
else:
p.type = 'BLUE'
p.value = cu(10)
print(subsession.get_group_matrix())
class Player(BasePlayer):
type = models.StringField()
value = models.CurrencyField()
class Group(BaseGroup):
pass
…
VT/VM: commented code
class Constants(BaseConstants):
# define here the constants of the session
name_in_url = 'groups_roles'
# label that appears in browser
players_per_group = 2
# how many players in each group (important for matching!)
num_rounds = 2
# how many repetitions (important for matching!)
matching = "Varying Type and Varying Matching (VT/VM)"
# matching protocol
import random
class Subsession(BaseSubsession):
pass
#group and types are defined in the Subsession class by the following function (method of the subsession class)
def creating_session(subsession):
#to set initial values in the subsession
subsession.group_randomly()
#group_randomly() -> built-in function that shuffles players
rdm=random.randint(0, 1)
# assign a random value, either 0 or 1, to variable rdm
for g in subsession.get_groups():
#get_groups() -> returns a list of all the groups in the subsession.
# loop through the groups in the subsession
for p in g.get_players():
# get_players() -> returns a list of all the players in the subsession.
# loop through the players in the subsession (p is a player)
#***************************************************************************
# matching and types when random is 1 - > even = BLUE, odd= GREEN
#***************************************************************************
if rdm==1:
# the following code is executed if rdm is 1
if p.id_in_group % 2 == 0:
# id_in_group -> player's attribute (unique identifier)
# if the id is even (via modulo operator)
p.type = 'BLUE'
# the participant is assigned to type "BLUE"
p.value = c(10)
else:
# if the participant id is odd
p.type = 'GREEN'
# the participant is assigned to type "GREEN"
p.value = c(0)
#***************************************************************************
# matching and types when random is 1 - > even = GREEN, odd= BLUE
#***************************************************************************
else:
# the following code is executed if rdm is 0
if p.id_in_group % 2 == 0:
# see comment above
p.type = 'GREEN'
# see comment above
p.value = c(0)
else:
# see comment above
p.type = 'BLUE'
# see comment above
p.value = c(10)
#***************************************************************************
# END code to generate matching and types
#***************************************************************************
class Group(BaseGroup):
pass
class Player(BasePlayer):
type = models.StringField()
value = models.CurrencyField()
# needed to store values A Public Goods Game
Setting
- Voluntary Contribution game
- Participants can decide how much contribute to a “public” project out of their endowment
- Participants are in a group of N subjects (usually 4)
- What is contributed is multiplied by an efficiency factor \(1/N < \alpha < 1\)
- What is not contributed is kept in a private account
- Private incentives are to contribute nothing to the public account
- But, contributions are efficient
- \(\Rightarrow\) social dilemma
- But, contributions are efficient
Parameters
- The interaction is repeated 10x in a partner fashion
- Total earnings are given by the sum of earnings in each stage
- Groups of 3
- Matched together for the entire experiment (partner)
- The efficiency factor \(\alpha=2/3\)
- The initial endowment is E=100
- The individual payoff function is
- \(\Pi_i=E-c_i+\alpha \sum_j^N c_j\)
- where, \(j\) are the members of \(i\)’s group (\(i\) include)
- \(\sum_j^N c_j\) is the size of the public project
- \(\Pi_i=E-c_i+\alpha \sum_j^N c_j\)
- The individual payoff function is
- Choices are in integer steps
- \(c_i \in \{0, 100\}\)
App
Screens
_inity_ (models): Constants
- Set here a few important constants
- Players per group
- Number of rounds
- Individual endowment
- Multiplier for the public project
- \(\alpha=\frac{2=multiplier}{3=group~members}\)
class C(BaseConstants):
NAME_IN_URL = 'PGG' # App name for URL
PLAYERS_PER_GROUP = 3 # Number of players per group
NUM_ROUNDS = 4 # Number of rounds
ENDOWMENT = cu(100) # Each player's initial endowment
MULTIPLIER = 2 # Multiplier for the public good_init.py_ (models): Matching
- A function that defines matching with reference to class
Subsession- No need to define roles in PGG
- Partner matching
- Random first round and then as first round
# Group players randomly in round 1, keep same groups in later rounds
def creating_session(subsession: Subsession):
if subsession.round_number == 1:
subsession.group_randomly()
else:
subsession.group_like_round(1)_init.py_ (models): Compute payoffs
- A function that defines matching with reference to class
Group- Typical of strategic interaction, while in individual decision making they are defined in Player
class Group(BaseGroup):
total_choices = models.CurrencyField() # Total contributions in the group
individual_share = models.CurrencyField() # Each player's share from the public good
total_earnings = models.CurrencyField() # Total earnings from the public good
# Calculate payoffs for each group after all have contributed
def set_payoffs(group: Group):
players = group.get_players()
choices = [p.choice for p in players] # List of all contributions
print(choices)
group.total_choices = sum(choices) # Total group contribution
group.total_earnings = group.total_choices * C.MULTIPLIER # Public good total
group.individual_share = group.total_earnings / C.PLAYERS_PER_GROUP # Each player's share
for p in players:
# Each player's payoff: what they kept + their share from the public good
p.payoff = C.ENDOWMENT - p.choice + group.individual_share_init.py_ (models): Compute final payoff
- Final payoff is the sum of payoffs in all rounds
[...]
# At the last round, sum up all payoffs for final result
if group.round_number == C.NUM_ROUNDS:
for p in players:
hist = p.in_all_rounds()
p.payoff_final = sum([g.payoff for g in hist])_init.py_ (models): Player’s variables
- Variables for players
[...]
# Player class: stores player-level variables for each round
class Player(BasePlayer):
choice = models.CurrencyField(min=0, max=C.ENDOWMENT) # Player's contribution
payoff_final = models.CurrencyField() # Sum of payoffs across all rounds
_init.py_ (pages):
- We have 5 pages
- page_sequence = [Instructions, Contribute, ResultsWaitPage, Results, FinalResults]
- Instructions(Page)
- Displayed only in round 1
- Contribute(Page)
- ResultsWaitPage(WaitPage)
- WaitPage → oTree waits until all players in the group have arrived at that point in the sequence, and then all players are allowed to proceed
- Important that we have all data before computing the payoffs!
- WaitPage → oTree waits until all players in the group have arrived at that point in the sequence, and then all players are allowed to proceed
- Results(Page)
- FinalResults(Page)
- Displayed only in last round
- Instructions(Page)
page_sequence = [Instructions, Contribute, ResultsWaitPage, Results, FinalResults]
Intructions(Page)
# Instructions page: only shown in round 1
class Instructions(Page):
@staticmethod
def is_displayed(player: Player):
return player.round_number == 1
@staticmethod
def vars_for_template(player: Player):
return {
'common_proj': C.ENDOWMENT * C.PLAYERS_PER_GROUP, # Total possible group endowment
}Contribute(Page)
- Contribute
- Collect own contribution to the public project
# Contribution page: player chooses how much to contribute
class Contribute(Page):
form_model = 'player'
form_fields = ['choice']
@staticmethod
def vars_for_template(player: Player):
return {
'round_number': player.round_number,
'endowment': C.ENDOWMENT,
}ResultsWaitPage(WaitPage)
- Need to “gather” all members of the group
- When all players arrive you apply the set_payoffs method (see above)
- If you do not use a waitpage you will not be able to get all chocies in a group!
# Wait page: waits for all players to contribute, then calculates payoffs
class ResultsWaitPage(WaitPage):
@staticmethod
def after_all_players_arrive(group: Group):
set_payoffs(group)Results(Page)
- Display results of current round
- Display history of choices
- A dynamic table
# Results page: shows outcome for the current round and history
class Results(Page):
@staticmethod
def vars_for_template(player: Player):
hist = player.in_all_rounds() # All rounds for this player
# Safely get group-level attributes for each round
history_contrib = [getattr(g.group, 'total_choices', 0) for g in hist] # List of total group contributions per round
# If you use a graph, you may need to import safe_json or use json.dumps
try:
from otree.api import safe_json
history_contrib_json = safe_json(history_contrib) # Convert to JSON for JavaScript graphing
except ImportError:
import json
history_contrib_json = json.dumps(history_contrib) # Fallback if safe_json not available
data_hist = [
[g.choice for g in hist], # Player's contributions per round
[getattr(g.group, 'individual_share', 0) for g in hist], # Player's share from public good per round
[g.payoff for g in hist] # Player's payoff per round
]
print(data_hist) # For debugging: print the data structure
table_hist = []
for j in range(0, player.round_number):
t = [j+1] # Round number (1-based)
for i in data_hist:
t.append(i[j]) # Add each data point for this round
table_hist.append(t) # Add row to history table
amount_kept = C.ENDOWMENT - player.choice # What player kept this round
return {
'history_contrib': history_contrib_json, # JSON for graph
'round_number': player.round_number, # Current round
'kept': amount_kept, # Amount kept this round
'table_hist': table_hist # Table of history for template
}FinalResults(Page)
- Final results
- Cumulative payment
class FinalResults(Page): # Page to display final cumulative results
@staticmethod
def is_displayed(player: Player): # Only show this page in the last round
return player.round_number == C.NUM_ROUNDS # True if current round is the last one
@staticmethod
def vars_for_template(player: Player): # Variables passed to the template
return {'payoff_final': player.payoff_final} # Player's total payoff across all roundsAppendix
Assignment 1
Objective
Explore the effect of different group matching protocols.
Tasks
- Using the
groups_rolesapp, implement this matching protocols:- Constant Matching (Partner Matching): for round 1 to 3 Groups are formed randomly in round 1,2, and 3
- Varying Matching (Stranger Matching): for round 4 to 6 Groups are randomly re-formed in every round.
Assignment 2
Objective
Change the payment rule in the PGG app so that, instead of summing payoffs across all rounds, only one round is randomly selected for payment at the end of the experiment.
Tasks
- Modify the PGG app code to randomly select one round for payment for each participant.
- Update the
FinalResults(Page)so it displays only the payoff from the selected round. - Update the instructions to explain the new payment rule.
- Test your implementation to ensure only the selected round’s payoff is paid and displayed.
Code
- The oTree apps of this lecture: